/*
 PubSubClient.h - A simple client for MQTT.
  Nick O'Leary
  http://knolleary.net
*/

#ifndef PubSubClient_h
#define PubSubClient_h

#include <Arduino.h>
#include "IPAddress.h"
#include "Client.h"
#include "Stream.h"

#define MQTT_VERSION_3_1 3
#define MQTT_VERSION_3_1_1 4

// MQTT_VERSION : Pick the version
//#define MQTT_VERSION MQTT_VERSION_3_1
#ifndef MQTT_VERSION
#define MQTT_VERSION MQTT_VERSION_3_1_1
#endif

// MQTT_MAX_PACKET_SIZE : Maximum packet size
#ifndef MQTT_MAX_PACKET_SIZE
#define MQTT_MAX_PACKET_SIZE 12800
#endif

// MQTT_KEEPALIVE : keepAlive interval in Seconds
#ifndef MQTT_KEEPALIVE
#define MQTT_KEEPALIVE 15
#endif

// MQTT_SOCKET_TIMEOUT: socket timeout interval in Seconds
#ifndef MQTT_SOCKET_TIMEOUT
#define MQTT_SOCKET_TIMEOUT 15
#endif

// MQTT_MAX_TRANSFER_SIZE : limit how much data is passed to the network client
//  in each write call. Needed for the Arduino Wifi Shield. Leave undefined to
//  pass the entire MQTT packet in each write call.
//#define MQTT_MAX_TRANSFER_SIZE 80

// Possible values for client.state()
#define MQTT_CONNECTION_TIMEOUT -4
#define MQTT_CONNECTION_LOST -3
#define MQTT_CONNECT_FAILED -2
#define MQTT_DISCONNECTED -1
#define MQTT_CONNECTED 0
#define MQTT_CONNECT_BAD_PROTOCOL 1
#define MQTT_CONNECT_BAD_CLIENT_ID 2
#define MQTT_CONNECT_UNAVAILABLE 3
#define MQTT_CONNECT_BAD_CREDENTIALS 4
#define MQTT_CONNECT_UNAUTHORIZED 5

#define MQTTCONNECT 1 << 4		// Client request to connect to Server
#define MQTTCONNACK 2 << 4		// Connect Acknowledgment
#define MQTTPUBLISH 3 << 4		// Publish message
#define MQTTPUBACK 4 << 4		// Publish Acknowledgment
#define MQTTPUBREC 5 << 4		// Publish Received (assured delivery part 1)
#define MQTTPUBREL 6 << 4		// Publish Release (assured delivery part 2)
#define MQTTPUBCOMP 7 << 4		// Publish Complete (assured delivery part 3)
#define MQTTSUBSCRIBE 8 << 4	// Client Subscribe request
#define MQTTSUBACK 9 << 4		// Subscribe Acknowledgment
#define MQTTUNSUBSCRIBE 10 << 4 // Client Unsubscribe request
#define MQTTUNSUBACK 11 << 4	// Unsubscribe Acknowledgment
#define MQTTPINGREQ 12 << 4		// PING Request
#define MQTTPINGRESP 13 << 4	// PING Response
#define MQTTDISCONNECT 14 << 4  // Client is Disconnecting
#define MQTTReserved 15 << 4	// Reserved

#define MQTTQOS0 (0 << 1)
#define MQTTQOS1 (1 << 1)
#define MQTTQOS2 (2 << 1)

// Maximum size of fixed header and variable length size header
#define MQTT_MAX_HEADER_SIZE 5

#if defined(ESP8266) || defined(ESP32)
#include <functional>
#define MQTT_CALLBACK_SIGNATURE std::function<void(char *, uint8_t *, unsigned int)> callback
#else
#define MQTT_CALLBACK_SIGNATURE void (*callback)(char *, uint8_t *, unsigned int)
#endif

#define CHECK_STRING_LENGTH(l, s)                 \
	if (l + 2 + strlen(s) > MQTT_MAX_PACKET_SIZE) \
	{                                             \
		_client->stop();                          \
		return false;                             \
	}

class PubSubClient : public Print
{
private:
	Client *_client;
	uint8_t buffer[MQTT_MAX_PACKET_SIZE];
	uint16_t nextMsgId;
	unsigned long lastOutActivity;
	unsigned long lastInActivity;
	bool pingOutstanding;
	MQTT_CALLBACK_SIGNATURE;
	uint16_t readPacket(uint8_t *);
	boolean readByte(uint8_t *result);
	boolean readByte(uint8_t *result, uint16_t *index);
	boolean write(uint8_t header, uint8_t *buf, uint16_t length);
	uint16_t writeString(const char *string, uint8_t *buf, uint16_t pos);
	// Build up the header ready to send
	// Returns the size of the header
	// Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start
	//       (MQTT_MAX_HEADER_SIZE - <returned size>) bytes into the buffer
	size_t buildHeader(uint8_t header, uint8_t *buf, uint16_t length);
	IPAddress ip;
	const char *domain;
	uint16_t port;
	Stream *stream;
	int _state;

public:
	PubSubClient();
	PubSubClient(Client &client);
	PubSubClient(IPAddress, uint16_t, Client &client);
	PubSubClient(IPAddress, uint16_t, Client &client, Stream &);
	PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client);
	PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client, Stream &);
	PubSubClient(uint8_t *, uint16_t, Client &client);
	PubSubClient(uint8_t *, uint16_t, Client &client, Stream &);
	PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client);
	PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client, Stream &);
	PubSubClient(const char *, uint16_t, Client &client);
	PubSubClient(const char *, uint16_t, Client &client, Stream &);
	PubSubClient(const char *, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client);
	PubSubClient(const char *, uint16_t, MQTT_CALLBACK_SIGNATURE, Client &client, Stream &);

	PubSubClient &setServer(IPAddress ip, uint16_t port);
	PubSubClient &setServer(uint8_t *ip, uint16_t port);
	PubSubClient &setServer(const char *domain, uint16_t port);
	PubSubClient &setCallback(MQTT_CALLBACK_SIGNATURE);
	PubSubClient &setClient(Client &client);
	PubSubClient &setStream(Stream &stream);

	boolean connect(const char *id);
	boolean connect(const char *id, const char *user, const char *pass);
	boolean connect(const char *id, const char *willTopic, uint8_t willQos, boolean willRetain, const char *willMessage);
	boolean connect(const char *id, const char *user, const char *pass, const char *willTopic, uint8_t willQos, boolean willRetain, const char *willMessage);
	boolean connect(const char *id, const char *user, const char *pass, const char *willTopic, uint8_t willQos, boolean willRetain, const char *willMessage, boolean cleanSession);
	void disconnect();
	boolean publish(const char *topic, const char *payload);
	boolean publish(const char *topic, const char *payload, boolean retained);
	boolean publish(const char *topic, const uint8_t *payload, unsigned int plength);
	boolean publish(const char *topic, const uint8_t *payload, unsigned int plength, boolean retained);
	boolean publish_P(const char *topic, const char *payload, boolean retained);
	boolean publish_P(const char *topic, const uint8_t *payload, unsigned int plength, boolean retained);
	// Start to publish a message.
	// This API:
	//   beginPublish(...)
	//   one or more calls to write(...)
	//   endPublish()
	// Allows for arbitrarily large payloads to be sent without them having to be copied into
	// a new buffer and held in memory at one time
	// Returns 1 if the message was started successfully, 0 if there was an error
	boolean beginPublish(const char *topic, unsigned int plength, boolean retained);
	// Finish off this publish message (started with beginPublish)
	// Returns 1 if the packet was sent successfully, 0 if there was an error
	int endPublish();
	// Write a single byte of payload (only to be used with beginPublish/endPublish)
	virtual size_t write(uint8_t);
	// Write size bytes from buffer into the payload (only to be used with beginPublish/endPublish)
	// Returns the number of bytes written
	virtual size_t write(const uint8_t *buffer, size_t size);
	boolean subscribe(const char *topic);
	boolean subscribe(const char *topic, uint8_t qos);
	boolean unsubscribe(const char *topic);
	boolean loop();
	boolean connected();
	int state();
};

#endif
